/*
 * eiam-application-oidc - Employee Identity and Access Management Program
 * Copyright © 2020-2023 TopIAM (support@topiam.cn)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package cn.topiam.employee.application.oidc;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.validation.ConstraintViolationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.IdGenerator;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Sets;

import cn.topiam.employee.application.exception.AppNotExistException;
import cn.topiam.employee.application.oidc.converter.AppOidcStandardConfigConverter;
import cn.topiam.employee.application.oidc.model.AppOidcStandardSaveConfigParam;
import cn.topiam.employee.audit.context.AuditContext;
import cn.topiam.employee.common.entity.app.AppEntity;
import cn.topiam.employee.common.entity.app.AppOidcConfigEntity;
import cn.topiam.employee.common.entity.app.po.AppOidcConfigPO;
import cn.topiam.employee.common.enums.app.*;
import cn.topiam.employee.common.repository.app.*;
import cn.topiam.employee.support.exception.TopIamException;
import cn.topiam.employee.support.util.BeanUtils;
import cn.topiam.employee.support.validation.ValidationHelp;
import static org.springframework.security.oauth2.core.ClientAuthenticationMethod.NONE;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

import static cn.topiam.employee.support.repository.domain.BaseEntity.LAST_MODIFIED_BY;
import static cn.topiam.employee.support.repository.domain.BaseEntity.LAST_MODIFIED_TIME;

/**
 * OIDC 用户应用
 *
 * @author TopIAM
 * Created by support@topiam.cn on  2022/8/20 23:20
 */
@SuppressWarnings("DuplicatedCode")
@Component
public class OidcStandardApplicationServiceImpl extends AbstractOidcApplicationService {
    private final Logger logger = LoggerFactory.getLogger(OidcStandardApplicationServiceImpl.class);

    /**
     * 创建应用
     *
     * @param name   {@link String} 名称
     * @param remark {@link String} 备注
     */
    @Override
    public String create(String name, String remark) {
        //1、创建应用
        AppEntity appEntity = new AppEntity();
        appEntity.setName(name);
        appEntity.setCode(
            org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(32).toLowerCase());
        appEntity.setTemplate(getCode());
        appEntity.setType(AppType.STANDARD);
        appEntity.setEnabled(true);
        appEntity.setProtocol(getProtocol());
        appEntity.setClientId(idGenerator.generateId().toString().replace("-", ""));
        appEntity.setClientSecret(idGenerator.generateId().toString().replace("-", ""));
        appEntity.setInitLoginType(InitLoginType.APP);
        appEntity.setAuthorizationType(AuthorizationType.AUTHORIZATION);
        appEntity.setRemark(remark);
        appRepository.save(appEntity);
        //2、创建证书
        createCertificate(appEntity.getId(), appEntity.getCode(), AppCertUsingType.OIDC_JWK);
        //3、创建配置
        AppOidcConfigEntity entity = new AppOidcConfigEntity();
        entity.setAppId(appEntity.getId());
        //客户端认证方法
        ClientAuthenticationMethod clientSecretBasic = ClientAuthenticationMethod.CLIENT_SECRET_BASIC;
        ClientAuthenticationMethod clientSecretPost = ClientAuthenticationMethod.CLIENT_SECRET_POST;
        entity.setClientAuthMethods(
            Sets.newHashSet(clientSecretBasic.getValue(), clientSecretPost.getValue()));
        //授予类型
        AuthorizationGrantType authorizationCodeGrantType = AuthorizationGrantType.AUTHORIZATION_CODE;
        AuthorizationGrantType refreshTokenGrantType = AuthorizationGrantType.REFRESH_TOKEN;
        AuthorizationGrantType clientCredentialsGrantType = AuthorizationGrantType.CLIENT_CREDENTIALS;
        entity.setAuthGrantTypes(Sets.newHashSet(authorizationCodeGrantType.getValue(),
            refreshTokenGrantType.getValue(), clientCredentialsGrantType.getValue()));
        entity.setResponseTypes(Sets.newHashSet(OAuth2AuthorizationResponseType.CODE.getValue()));
        //重定向URLs
        entity.setRedirectUris(Sets.newLinkedHashSet());
        //Scopes
        entity.setGrantScopes(Sets.newHashSet(OidcScopes.OPENID));
        //授权是否需要同意
        entity.setRequireAuthConsent(false);
        //PKCE
        entity.setRequireProofKey(false);
        //Token 端点认证签名算法
        entity.setTokenEndpointAuthSigningAlgorithm(SignatureAlgorithm.RS256.getName());
        //刷新 Token 过期时间 默认（30天）
        entity.setRefreshTokenTimeToLive(43200);
        //访问 Token 过期时间 默认（15天）
        entity.setAccessTokenTimeToLive(21600);
        //Access Token 格式
        entity.setAccessTokenFormat("reference");
        //30分钟
        entity.setIdTokenTimeToLive(30);
        //ID Token签名算法
        entity.setIdTokenSignatureAlgorithm(SignatureAlgorithm.RS256.getName());
        //是否重用刷新令牌
        entity.setReuseRefreshToken(false);
        appOidcConfigRepository.save(entity);
        return entity.getAppId().toString();
    }

    /**
     * 更新应用配置
     *
     * @param appId {@link String}
     * @param config {@link Map}
     */
    @Override
    public void saveConfig(String appId, Map<String, Object> config) {
        AppOidcStandardSaveConfigParam model;
        try {
            ObjectMapper mapper = new ObjectMapper();
            String value = mapper.writeValueAsString(config);
            // 指定序列化输入的类型
            mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
            model = mapper.readValue(value, AppOidcStandardSaveConfigParam.class);
        } catch (Exception e) {
            throw new TopIamException(e.getMessage());
        }
        //@formatter:off
        ValidationHelp.ValidationResult<AppOidcStandardSaveConfigParam> validationResult = ValidationHelp.validateEntity(model);
        if (validationResult.isHasErrors()) {
            throw new ConstraintViolationException(validationResult.getConstraintViolations());
        }
        //@formatter:on
        //1、修改基本信息
        Optional<AppEntity> optional = appRepository.findById(Long.valueOf(appId));
        if (optional.isEmpty()) {
            AuditContext.setContent("保存配置失败，应用 [" + appId + "] 不存在！");
            logger.error(AuditContext.getContent());
            throw new AppNotExistException();
        }
        AppEntity appEntity = optional.get();
        appEntity.setAuthorizationType(model.getAuthorizationType());
        appEntity.setInitLoginUrl(model.getInitLoginUrl());
        appEntity.setInitLoginType(model.getInitLoginType());
        appRepository.save(appEntity);
        //2、修改 OIDC 配置
        Optional<AppOidcConfigEntity> oidc = appOidcConfigRepository
            .findByAppId(Long.valueOf(appId));
        if (oidc.isEmpty()) {
            AuditContext.setContent("保存配置失败，应用 [" + appId + "] 不存在！");
            logger.error(AuditContext.getContent());
            throw new AppNotExistException();
        }
        AppOidcConfigEntity entity = oidc.get();
        AppOidcConfigEntity oidcConfig = appOidcStandardConfigConverter
            .appOidcStandardSaveConfigParamToEntity(model);
        BeanUtils.merge(oidcConfig, entity, LAST_MODIFIED_BY, LAST_MODIFIED_TIME);
        //开启PKCE
        if (entity.getRequireProofKey()) {
            entity.getClientAuthMethods().add(NONE.getValue());
        } else {
            entity.getClientAuthMethods().remove(NONE.getValue());
        }
        appOidcConfigRepository.save(entity);
    }

    /**
     * 获取配置
     *
     * @param appId {@link String}
     * @return {@link AppOidcConfigPO}
     */
    @Override
    public Object getConfig(String appId) {
        AppOidcConfigPO po = appOidcConfigRepository.getByAppId(Long.valueOf(appId));
        return appOidcStandardConfigConverter.entityConverterToOidcConfigResult(po);
    }

    /**
     * 获取应用标志
     *
     * @return {@link String}
     */
    @Override
    public String getCode() {
        return "oidc";
    }

    /**
     * 获取应用名称
     *
     * @return {@link String}
     */
    @Override
    public String getName() {
        return "OIDC";
    }

    /**
     * 获取应用描述
     *
     * @return {@link String}
     */
    @Override
    public String getDescription() {
        return "OIDC是OpenID Connect的简称，OIDC=(Identity, Authentication) + OAuth 2.0。它在OAuth2上构建了一个身份层，是一个基于OAuth2协议的身份认证标准协议。OIDC是一个协议族，提供很多的标准协议，包括Core核心协议和一些扩展协议。";
    }

    /**
     * 获取应用类型
     *
     * @return {@link AppType}
     */
    @Override
    public AppType getType() {
        return AppType.STANDARD;
    }

    /**
     * 获取应用协议
     *
     * @return {@link AppProtocol}
     */
    @Override
    public AppProtocol getProtocol() {
        return AppProtocol.OIDC;
    }

    /**
     * 获取表单Schema
     *
     * @return {@link Map}
     */
    @Override
    public List<Map> getFormSchema() {
        return null;
    }

    /**
     * 获取base64图标
     *
     * @return {@link String}
     */
    @Override
    public String getBase64Icon() {
        return "";
    }

    /**
     * IdGenerator
     */
    private final IdGenerator                    idGenerator = new AlternativeJdkIdGenerator();
    private final AppOidcStandardConfigConverter appOidcStandardConfigConverter;

    protected OidcStandardApplicationServiceImpl(AppCertRepository appCertRepository,
                                                 AppAccountRepository appAccountRepository,
                                                 AppAccessPolicyRepository appAccessPolicyRepository,
                                                 AppRepository appRepository,
                                                 AppOidcConfigRepository appOidcConfigRepository,
                                                 AppOidcStandardConfigConverter appOidcStandardConfigConverter) {
        super(appCertRepository, appAccountRepository, appAccessPolicyRepository, appRepository,
            appOidcConfigRepository);
        this.appOidcStandardConfigConverter = appOidcStandardConfigConverter;
    }
}
